package ru.ifmo.genetics.automaton.synchronizeable;

import org.uncommons.maths.number.NumberGenerator;
import org.uncommons.watchmaker.framework.EvaluatedCandidate;
import ru.ifmo.automaton.Automaton;
import ru.ifmo.common.Source;
import ru.ifmo.common.util.MathUtil;
import ru.ifmo.genetics.adaptation.AdaptableParameter;
import ru.ifmo.genetics.automaton.synchronizeable.fitness.StandartFitness;
import ru.ifmo.genetics.util.WeightedValuesNumberGenerator;

import java.util.*;

/**
 * @author Roman Kolganov
 *         11.12.11
 */
public class AdaptableAlphabetSymbolGenerator implements NumberGenerator<Integer>, AdaptableParameter<String> {

    private NumberGenerator<Integer> delegateGenerator;
    private final Source<Automaton<Character, String>> automatonSource;

    public AdaptableAlphabetSymbolGenerator(Source<Automaton<Character, String>> automatonSource) {
        this.delegateGenerator = automatonAlphabetSymbolGenerator(automatonSource);
        this.automatonSource = automatonSource;
    }

    public Integer nextValue() {
        return delegateGenerator.nextValue();
    }

    public void adapt(List<List<EvaluatedCandidate<String>>> lastGenerations) {
        if (meanDF(lastGenerations.get(0)) < meanDF(lastGenerations.get(1))) {
            delegateGenerator = empiricAlphabetSymbolGenerator(lastGenerations.get(1),
                    automatonSource.create().getAlphabet());
        }
    }

    private double meanDF(List<EvaluatedCandidate<String>> generation) {
        double DFSum = 0;
        for (EvaluatedCandidate<String> candidate : generation) {
            DFSum += StandartFitness.df(automatonSource.create(), candidate.getCandidate());
        }
        return DFSum / generation.size();
    }

    public static WeightedValuesNumberGenerator empiricAlphabetSymbolGenerator(
            List<EvaluatedCandidate<String>> goodPopulation, List<Character> alphabet) {
        return new WeightedValuesNumberGenerator(computeWeights(goodPopulation, alphabet));
    }

    public static WeightedValuesNumberGenerator automatonAlphabetSymbolGenerator(
            Source<Automaton<Character, String>> automatonSource) {
        return new WeightedValuesNumberGenerator(computeWeights(automatonSource));
    }

    private static Iterable<Long> computeWeights(Source<Automaton<Character, String>> automatonSource) {
        Automaton<Character, String> automaton = automatonSource.create();
        List<Character> alphabet = automaton.getAlphabet();
        List<Long> weights = new ArrayList<Long>(alphabet.size());
        for (Character symbol : alphabet) {
            weights.add(computeWeight(automaton, symbol));
        }
        return weights;
    }

    private static long computeWeight(Automaton<Character, String> automaton, Character symbol) {
        List<Integer> cyclesLengths = new ArrayList<Integer>();
        Set<String> processedStates = new HashSet<String>();
        Map<String, Integer> distanceFromCycle = new HashMap<String, Integer>(automaton.getStatesCount() * 2);
        for (String state : automaton) {
            if (!processedStates.contains(state)) {
                findCyclesReachableFromState(automaton, symbol, cyclesLengths, processedStates, distanceFromCycle,
                        state);
            }
        }
        int maxDistanceFromCycle = 0;
        for (int distance : distanceFromCycle.values()) {
            if (distance > maxDistanceFromCycle) {
                maxDistanceFromCycle = distance;
            }
        }
        return MathUtil.lowestCommonMultiple(cyclesLengths) + maxDistanceFromCycle;
    }

    private static void findCyclesReachableFromState(Automaton<Character, String> automaton, Character symbol,
                                                     List<Integer> cyclesLengths, Set<String> processedStates,
                                                     Map<String, Integer> distanceFromCycle, String state) {
        automaton.goToState(state);
        Map<String, Integer> path = new HashMap<String, Integer>(automaton.getStatesCount() * 2);
        String currentState = state;
        while (!processedStates.contains(currentState)) {
            processedStates.add(currentState);
            path.put(currentState, path.size());
            currentState = automaton.step(symbol);
        }
        if (path.containsKey(currentState)) {
            int pathBeforeCycleLength = path.get(currentState);
            cyclesLengths.add(path.size() - pathBeforeCycleLength);
            distanceFromCycle.put(state, pathBeforeCycleLength);
        } else {
            int distanceBetweenCurrentAndCycle = 0;
            if (distanceFromCycle.containsKey(currentState)) {
                distanceBetweenCurrentAndCycle = distanceFromCycle.get(currentState);
            }
            distanceFromCycle.put(state, path.size() + distanceBetweenCurrentAndCycle);
        }
    }

    private static Iterable<Long> computeWeights(List<EvaluatedCandidate<String>> goodPopulation,
                                                 List<Character> alphabet) {
        Long[] weights = new Long[alphabet.size()];
        Map<Character, Integer> indexes = new HashMap<Character, Integer>(alphabet.size() * 2);
        for (int i = 0; i < alphabet.size(); i++) {
            weights[i] = 0l;
            indexes.put(alphabet.get(i), i);
        }
        for (EvaluatedCandidate<String> evaluatedIndividual : goodPopulation) {
            String individual = evaluatedIndividual.getCandidate();
            for (int i = 0; i < individual.length(); i++) {
                int index = indexes.get(individual.charAt(i));
                weights[index]++;
            }
        }
        return Arrays.asList(weights);
    }

}
